@intranefr/superbackend 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +91 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +8 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +528 -10
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
package/.env.example
CHANGED
|
@@ -50,3 +50,8 @@ MAX_FILE_SIZE_HARD_CAP=10485760
|
|
|
50
50
|
# Encryption key for encrypted settings (new preferred name)
|
|
51
51
|
# SUPERBACKEND_ENCRYPTION_KEY=your-32-byte-encryption-key
|
|
52
52
|
# Legacy fallback: SAASBACKEND_ENCRYPTION_KEY=your-32-byte-encryption-key
|
|
53
|
+
|
|
54
|
+
# Console Manager
|
|
55
|
+
# Set to 'false' to disable console manager initialization
|
|
56
|
+
# When disabled, console methods are not overridden and no console entries are tracked
|
|
57
|
+
# CONSOLE_MANAGER_ENABLED=true
|
package/README.md
CHANGED
|
@@ -23,6 +23,13 @@ Node.js middleware that gives your project backend superpowers. Handles authenti
|
|
|
23
23
|
- **Webhooks**: Outgoing webhook system for event-driven integrations
|
|
24
24
|
- **Metrics & Activity**: Usage tracking and analytics for business insights
|
|
25
25
|
- **Middleware Mode**: Drop-in Express middleware that preserves your app structure
|
|
26
|
+
- **Workflows System**: Node-based automation with LLM integration, conditionals, and HTTP calls
|
|
27
|
+
- **LLM UI Integration**: AI-powered UI components and conversational interfaces
|
|
28
|
+
- **Admin Scripts & Terminals**: Operational tooling for script execution and terminal management
|
|
29
|
+
- **Migration System**: Database migration and data transfer between environments
|
|
30
|
+
- **Upload Namespaces**: Advanced file organization with customizable storage rules
|
|
31
|
+
- **UI Components**: Project-scoped reusable UI widgets with browser SDK integration
|
|
32
|
+
- **UI Components AI Assistance**: AI-powered generation and iteration of UI component code
|
|
26
33
|
|
|
27
34
|
---
|
|
28
35
|
|
|
@@ -85,6 +92,10 @@ See the `docs/features/` directory for detailed guides:
|
|
|
85
92
|
- [Core Configuration](docs/features/core-configuration.md)
|
|
86
93
|
- [Admin API Usage](docs/features/admin-api-usage.md)
|
|
87
94
|
- [Billing & Subscriptions](docs/features/billing-and-subscriptions.md)
|
|
95
|
+
- [Workflows System](docs/features/workflows-system.md)
|
|
96
|
+
- [LLM UI Integration](docs/features/llm-ui-integration.md)
|
|
97
|
+
- [Migration System](docs/features/migration-system.md)
|
|
98
|
+
- [Upload Namespaces](docs/features/upload-namespaces.md)
|
|
88
99
|
|
|
89
100
|
---
|
|
90
101
|
|
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const express = require("express");
|
|
|
11
11
|
* @returns {express.Router} Configured Express router
|
|
12
12
|
*/
|
|
13
13
|
const middleware = require("./src/middleware");
|
|
14
|
+
const { attachTerminalWebsocketServer } = require('./src/services/terminalsWs.service');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Creates and starts a standalone SuperBackend server
|
|
@@ -24,13 +25,26 @@ function startServer(options = {}) {
|
|
|
24
25
|
const app = express();
|
|
25
26
|
const PORT = options.port || process.env.PORT || 3000;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
const router = module.exports.middleware(options);
|
|
29
|
+
app.use(router);
|
|
28
30
|
|
|
29
31
|
// Start server
|
|
30
32
|
const server = app.listen(PORT, () => {
|
|
31
33
|
console.log(`🚀 SuperBackend standalone server running on http://localhost:${PORT}`);
|
|
32
34
|
});
|
|
33
35
|
|
|
36
|
+
// Attach WebSocket server via middleware helper or directly
|
|
37
|
+
console.log('[Index] Attaching WebSocket server...');
|
|
38
|
+
if (typeof router.attachWs === 'function') {
|
|
39
|
+
console.log('[Index] Using router.attachWs');
|
|
40
|
+
router.attachWs(server);
|
|
41
|
+
} else {
|
|
42
|
+
// Fallback: attach directly with admin path
|
|
43
|
+
const adminPath = router.adminPath || '/admin';
|
|
44
|
+
console.log('[Index] Using fallback attach with adminPath:', adminPath);
|
|
45
|
+
attachTerminalWebsocketServer(server, { basePathPrefix: adminPath });
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
return { app, server };
|
|
35
49
|
}
|
|
36
50
|
|
|
@@ -48,6 +62,8 @@ const saasbackend = {
|
|
|
48
62
|
storage: require("./src/services/storage"),
|
|
49
63
|
i18n: require("./src/services/i18n.service"),
|
|
50
64
|
audit: require("./src/services/audit.service"),
|
|
65
|
+
cacheLayer: require("./src/services/cacheLayer.service"),
|
|
66
|
+
rbac: require("./src/services/rbac.service"),
|
|
51
67
|
globalSettings: require("./src/services/globalSettings.service"),
|
|
52
68
|
jsonConfigs: require("./src/services/jsonConfigs.service"),
|
|
53
69
|
assets: require("./src/services/assets.service"),
|
|
@@ -58,12 +74,24 @@ const saasbackend = {
|
|
|
58
74
|
forms: require("./src/services/forms.service"),
|
|
59
75
|
webhooks: require("./src/services/webhook.service"),
|
|
60
76
|
workflow: require("./src/services/workflow.service"),
|
|
77
|
+
healthChecks: require("./src/services/healthChecks.service"),
|
|
78
|
+
dbBrowser: require("./src/services/dbBrowser.service"),
|
|
79
|
+
rateLimiter: require("./src/services/rateLimiter.service"),
|
|
61
80
|
},
|
|
62
81
|
models: {
|
|
63
82
|
ActionEvent: require("./src/models/ActionEvent"),
|
|
64
83
|
ActivityLog: require("./src/models/ActivityLog"),
|
|
65
84
|
Asset: require("./src/models/Asset"),
|
|
66
85
|
AuditEvent: require("./src/models/AuditEvent"),
|
|
86
|
+
CacheEntry: require("./src/models/CacheEntry"),
|
|
87
|
+
RateLimitCounter: require("./src/models/RateLimitCounter"),
|
|
88
|
+
RateLimitMetricBucket: require("./src/models/RateLimitMetricBucket"),
|
|
89
|
+
RbacRole: require("./src/models/RbacRole"),
|
|
90
|
+
RbacUserRole: require("./src/models/RbacUserRole"),
|
|
91
|
+
RbacGroup: require("./src/models/RbacGroup"),
|
|
92
|
+
RbacGroupMember: require("./src/models/RbacGroupMember"),
|
|
93
|
+
RbacGroupRole: require("./src/models/RbacGroupRole"),
|
|
94
|
+
RbacGrant: require("./src/models/RbacGrant"),
|
|
67
95
|
EmailLog: require("./src/models/EmailLog"),
|
|
68
96
|
ErrorAggregate: require("./src/models/ErrorAggregate"),
|
|
69
97
|
FormSubmission: require("./src/models/FormSubmission"),
|
|
@@ -85,12 +113,22 @@ const saasbackend = {
|
|
|
85
113
|
Webhook: require("./src/models/Webhook"),
|
|
86
114
|
Workflow: require("./src/models/Workflow"),
|
|
87
115
|
WorkflowExecution: require("./src/models/WorkflowExecution"),
|
|
116
|
+
|
|
117
|
+
HealthCheck: require("./src/models/HealthCheck"),
|
|
118
|
+
HealthCheckRun: require("./src/models/HealthCheckRun"),
|
|
119
|
+
HealthIncident: require("./src/models/HealthIncident"),
|
|
120
|
+
HealthAutoHealAttempt: require("./src/models/HealthAutoHealAttempt"),
|
|
121
|
+
|
|
122
|
+
ExternalDbConnection: require("./src/models/ExternalDbConnection"),
|
|
88
123
|
},
|
|
89
124
|
helpers: {
|
|
90
125
|
auth: require("./src/middleware/auth"),
|
|
91
126
|
org: require("./src/middleware/org"),
|
|
127
|
+
rbac: require("./src/middleware/rbac"),
|
|
92
128
|
i18n: require("./src/services/i18n.service"),
|
|
93
129
|
jsonConfigs: require("./src/services/jsonConfigs.service"),
|
|
130
|
+
terminals: require("./src/services/terminalsWs.service"),
|
|
131
|
+
rateLimiter: require("./src/services/rateLimiter.service"),
|
|
94
132
|
},
|
|
95
133
|
};
|
|
96
134
|
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intranefr/superbackend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Node.js middleware that gives your project backend superpowers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node server.js",
|
|
8
|
-
"dev": "nodemon server.js",
|
|
8
|
+
"dev": "nodemon --verbose --ignore uploads --ignore '*.log' server.js",
|
|
9
9
|
"start:minio": "docker compose -f compose.standalone.yml --profile minio-only up -d minio",
|
|
10
10
|
"minio:envs": "node -e \"console.log(['S3_ENDPOINT=http://localhost:9000','S3_REGION=us-east-1','S3_ACCESS_KEY_ID=minioadmin','S3_SECRET_ACCESS_KEY=minioadmin','S3_BUCKET=saasbackend','S3_FORCE_PATH_STYLE=true'].join('\\n'))\"",
|
|
11
11
|
"build:sdk:error-tracking:browser": "esbuild sdk/error-tracking/browser/src/embed.js --bundle --format=iife --global-name=saasbackendErrorTrackingEmbed --outfile=sdk/error-tracking/browser/dist/embed.iife.js",
|
|
12
|
+
"build:sdk:ui-components:browser": "esbuild sdk/ui-components/browser/src/index.js --bundle --format=iife --outfile=public/sdk/ui-components.iife.js",
|
|
12
13
|
"test": "jest",
|
|
13
14
|
"test:watch": "jest --watch",
|
|
14
15
|
"test:coverage": "jest --coverage"
|
|
@@ -35,17 +36,24 @@
|
|
|
35
36
|
"bcryptjs": "^2.4.3",
|
|
36
37
|
"cheerio": "^1.0.0-rc.12",
|
|
37
38
|
"cors": "^2.8.5",
|
|
39
|
+
"cron-parser": "3.5.0",
|
|
38
40
|
"dotenv": "^16.3.1",
|
|
39
41
|
"ejs": "^3.1.9",
|
|
40
42
|
"express": "^4.18.2",
|
|
41
43
|
"jsonwebtoken": "^9.0.2",
|
|
44
|
+
"marked": "^4.3.0",
|
|
42
45
|
"mongoose": "^8.0.0",
|
|
43
46
|
"multer": "^1.4.5-lts.1",
|
|
47
|
+
"mysql2": "^3.16.1",
|
|
48
|
+
"node-cron": "^4.2.1",
|
|
49
|
+
"node-pty": "^1.1.0",
|
|
44
50
|
"openai": "^4.0.0",
|
|
51
|
+
"redis": "^4.7.1",
|
|
45
52
|
"resend": "^6.4.0",
|
|
46
53
|
"ssh2-sftp-client": "^12.0.1",
|
|
47
54
|
"stripe": "^14.0.0",
|
|
48
|
-
"vm2": "^3.10.0"
|
|
55
|
+
"vm2": "^3.10.0",
|
|
56
|
+
"ws": "^8.18.0"
|
|
49
57
|
},
|
|
50
58
|
"devDependencies": {
|
|
51
59
|
"esbuild": "^0.25.0",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// sdk/ui-components/browser/src/index.js
|
|
3
|
+
(function() {
|
|
4
|
+
function toStr(v) {
|
|
5
|
+
return v === void 0 || v === null ? "" : String(v);
|
|
6
|
+
}
|
|
7
|
+
function normalizeApiUrl(apiUrl) {
|
|
8
|
+
const u = toStr(apiUrl).trim();
|
|
9
|
+
if (!u) return "";
|
|
10
|
+
return u.replace(/\/$/, "");
|
|
11
|
+
}
|
|
12
|
+
function buildHeaders(apiKey) {
|
|
13
|
+
const headers = {};
|
|
14
|
+
const key = toStr(apiKey).trim();
|
|
15
|
+
if (key) headers["x-project-key"] = key;
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
async function fetchJson(url, headers) {
|
|
19
|
+
const res = await fetch(url, { headers: headers || {} });
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
let data = null;
|
|
22
|
+
try {
|
|
23
|
+
data = text ? JSON.parse(text) : null;
|
|
24
|
+
} catch {
|
|
25
|
+
data = null;
|
|
26
|
+
}
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const msg = data && data.error ? data.error : "Request failed";
|
|
29
|
+
const err = new Error(msg);
|
|
30
|
+
err.status = res.status;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
function ensureTemplate(code, html) {
|
|
36
|
+
const id = "ui-cmp-" + code;
|
|
37
|
+
let tpl = document.getElementById(id);
|
|
38
|
+
if (!tpl) {
|
|
39
|
+
tpl = document.createElement("template");
|
|
40
|
+
tpl.id = id;
|
|
41
|
+
document.body.appendChild(tpl);
|
|
42
|
+
}
|
|
43
|
+
tpl.innerHTML = toStr(html);
|
|
44
|
+
return tpl;
|
|
45
|
+
}
|
|
46
|
+
function injectCssScoped(code, cssText) {
|
|
47
|
+
const id = "ui-cmp-style-" + code;
|
|
48
|
+
let el = document.getElementById(id);
|
|
49
|
+
if (!el) {
|
|
50
|
+
el = document.createElement("style");
|
|
51
|
+
el.id = id;
|
|
52
|
+
document.head.appendChild(el);
|
|
53
|
+
}
|
|
54
|
+
el.textContent = toStr(cssText);
|
|
55
|
+
}
|
|
56
|
+
function createShadowRootContainer() {
|
|
57
|
+
const host = document.createElement("div");
|
|
58
|
+
host.style.all = "initial";
|
|
59
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
60
|
+
return { host, shadow };
|
|
61
|
+
}
|
|
62
|
+
function defaultMountTarget() {
|
|
63
|
+
return document.body;
|
|
64
|
+
}
|
|
65
|
+
function compileComponentJs(jsCode) {
|
|
66
|
+
const code = toStr(jsCode);
|
|
67
|
+
if (!code.trim()) {
|
|
68
|
+
return function() {
|
|
69
|
+
return {};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return new Function("api", "templateRootEl", "props", code);
|
|
73
|
+
}
|
|
74
|
+
const state = {
|
|
75
|
+
initialized: false,
|
|
76
|
+
projectId: null,
|
|
77
|
+
apiKey: null,
|
|
78
|
+
apiUrl: "",
|
|
79
|
+
cssIsolation: "scoped",
|
|
80
|
+
components: {}
|
|
81
|
+
};
|
|
82
|
+
function registerComponent(def) {
|
|
83
|
+
const code = toStr(def.code).trim().toLowerCase();
|
|
84
|
+
if (!code) return;
|
|
85
|
+
const version = def.version;
|
|
86
|
+
const html = def.html;
|
|
87
|
+
const js = def.js;
|
|
88
|
+
const css = def.css;
|
|
89
|
+
ensureTemplate(code, html);
|
|
90
|
+
const component = {
|
|
91
|
+
code,
|
|
92
|
+
version,
|
|
93
|
+
css,
|
|
94
|
+
js,
|
|
95
|
+
create: function(props, options) {
|
|
96
|
+
const opts = options || {};
|
|
97
|
+
const mountEl = opts.mountEl || defaultMountTarget();
|
|
98
|
+
const isolation = opts.cssIsolation || state.cssIsolation;
|
|
99
|
+
const tpl = ensureTemplate(code, html);
|
|
100
|
+
const fragment = tpl.content.cloneNode(true);
|
|
101
|
+
let templateRootEl;
|
|
102
|
+
let instanceRoot;
|
|
103
|
+
let shadow = null;
|
|
104
|
+
if (isolation === "shadow") {
|
|
105
|
+
const c = createShadowRootContainer();
|
|
106
|
+
instanceRoot = c.host;
|
|
107
|
+
shadow = c.shadow;
|
|
108
|
+
templateRootEl = shadow;
|
|
109
|
+
if (css) {
|
|
110
|
+
const style = document.createElement("style");
|
|
111
|
+
style.textContent = toStr(css);
|
|
112
|
+
shadow.appendChild(style);
|
|
113
|
+
}
|
|
114
|
+
shadow.appendChild(fragment);
|
|
115
|
+
} else {
|
|
116
|
+
instanceRoot = document.createElement("div");
|
|
117
|
+
templateRootEl = instanceRoot;
|
|
118
|
+
if (css) injectCssScoped(code, css);
|
|
119
|
+
instanceRoot.appendChild(fragment);
|
|
120
|
+
}
|
|
121
|
+
mountEl.appendChild(instanceRoot);
|
|
122
|
+
const api = {
|
|
123
|
+
unmount: function() {
|
|
124
|
+
try {
|
|
125
|
+
instanceRoot.remove();
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
mountEl,
|
|
130
|
+
hostEl: instanceRoot,
|
|
131
|
+
shadowRoot: shadow
|
|
132
|
+
};
|
|
133
|
+
const fn = compileComponentJs(js);
|
|
134
|
+
let exported = {};
|
|
135
|
+
try {
|
|
136
|
+
exported = fn(api, templateRootEl, props || {}) || {};
|
|
137
|
+
} catch (e) {
|
|
138
|
+
exported = {
|
|
139
|
+
error: e
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return Object.assign({ api }, exported);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
state.components[code] = component;
|
|
146
|
+
uiCmp[code] = component;
|
|
147
|
+
uiComponents[code] = component;
|
|
148
|
+
}
|
|
149
|
+
async function init(options) {
|
|
150
|
+
const opts = options || {};
|
|
151
|
+
const projectId = toStr(opts.projectId).trim();
|
|
152
|
+
if (!projectId) throw new Error("projectId is required");
|
|
153
|
+
const apiUrl = normalizeApiUrl(opts.apiUrl);
|
|
154
|
+
const apiKey = opts.apiKey;
|
|
155
|
+
state.projectId = projectId;
|
|
156
|
+
state.apiKey = apiKey;
|
|
157
|
+
state.apiUrl = apiUrl;
|
|
158
|
+
const cssIsolation = toStr(opts.cssIsolation || "scoped").trim().toLowerCase();
|
|
159
|
+
state.cssIsolation = cssIsolation === "shadow" ? "shadow" : "scoped";
|
|
160
|
+
const base = state.apiUrl;
|
|
161
|
+
const url = base + "/api/ui-components/projects/" + encodeURIComponent(projectId) + "/manifest";
|
|
162
|
+
const data = await fetchJson(url, buildHeaders(apiKey));
|
|
163
|
+
const items = data && Array.isArray(data.components) ? data.components : [];
|
|
164
|
+
for (const def of items) {
|
|
165
|
+
registerComponent(def);
|
|
166
|
+
}
|
|
167
|
+
state.initialized = true;
|
|
168
|
+
return { project: data ? data.project : null, count: items.length };
|
|
169
|
+
}
|
|
170
|
+
async function load(code) {
|
|
171
|
+
const c = toStr(code).trim().toLowerCase();
|
|
172
|
+
if (!c) throw new Error("code is required");
|
|
173
|
+
if (!state.projectId) throw new Error("uiCmp not initialized");
|
|
174
|
+
if (state.components[c]) return state.components[c];
|
|
175
|
+
const base = state.apiUrl;
|
|
176
|
+
const url = base + "/api/ui-components/projects/" + encodeURIComponent(state.projectId) + "/components/" + encodeURIComponent(c);
|
|
177
|
+
const data = await fetchJson(url, buildHeaders(state.apiKey));
|
|
178
|
+
if (!data || !data.component) throw new Error("Component not found");
|
|
179
|
+
registerComponent(data.component);
|
|
180
|
+
return state.components[c];
|
|
181
|
+
}
|
|
182
|
+
const uiCmp = {
|
|
183
|
+
init,
|
|
184
|
+
load,
|
|
185
|
+
_state: state
|
|
186
|
+
};
|
|
187
|
+
const uiComponents = uiCmp;
|
|
188
|
+
window.uiCmp = uiCmp;
|
|
189
|
+
window.uiComponents = uiComponents;
|
|
190
|
+
})();
|
|
191
|
+
})();
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
function toStr(v) {
|
|
3
|
+
return v === undefined || v === null ? '' : String(v);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function normalizeApiUrl(apiUrl) {
|
|
7
|
+
const u = toStr(apiUrl).trim();
|
|
8
|
+
if (!u) return '';
|
|
9
|
+
return u.replace(/\/$/, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildHeaders(apiKey) {
|
|
13
|
+
const headers = {};
|
|
14
|
+
const key = toStr(apiKey).trim();
|
|
15
|
+
if (key) headers['x-project-key'] = key;
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function fetchJson(url, headers) {
|
|
20
|
+
const res = await fetch(url, { headers: headers || {} });
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
let data = null;
|
|
23
|
+
try {
|
|
24
|
+
data = text ? JSON.parse(text) : null;
|
|
25
|
+
} catch {
|
|
26
|
+
data = null;
|
|
27
|
+
}
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const msg = data && data.error ? data.error : 'Request failed';
|
|
30
|
+
const err = new Error(msg);
|
|
31
|
+
err.status = res.status;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureTemplate(code, html) {
|
|
38
|
+
const id = 'ui-cmp-' + code;
|
|
39
|
+
let tpl = document.getElementById(id);
|
|
40
|
+
if (!tpl) {
|
|
41
|
+
tpl = document.createElement('template');
|
|
42
|
+
tpl.id = id;
|
|
43
|
+
document.body.appendChild(tpl);
|
|
44
|
+
}
|
|
45
|
+
tpl.innerHTML = toStr(html);
|
|
46
|
+
return tpl;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function injectCssScoped(code, cssText) {
|
|
50
|
+
const id = 'ui-cmp-style-' + code;
|
|
51
|
+
let el = document.getElementById(id);
|
|
52
|
+
if (!el) {
|
|
53
|
+
el = document.createElement('style');
|
|
54
|
+
el.id = id;
|
|
55
|
+
document.head.appendChild(el);
|
|
56
|
+
}
|
|
57
|
+
el.textContent = toStr(cssText);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createShadowRootContainer() {
|
|
61
|
+
const host = document.createElement('div');
|
|
62
|
+
host.style.all = 'initial';
|
|
63
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
64
|
+
return { host, shadow };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function defaultMountTarget() {
|
|
68
|
+
return document.body;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compileComponentJs(jsCode) {
|
|
72
|
+
const code = toStr(jsCode);
|
|
73
|
+
if (!code.trim()) {
|
|
74
|
+
return function () {
|
|
75
|
+
return {};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return new Function('api', 'templateRootEl', 'props', code);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const state = {
|
|
82
|
+
initialized: false,
|
|
83
|
+
projectId: null,
|
|
84
|
+
apiKey: null,
|
|
85
|
+
apiUrl: '',
|
|
86
|
+
cssIsolation: 'scoped',
|
|
87
|
+
components: {},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function registerComponent(def) {
|
|
91
|
+
const code = toStr(def.code).trim().toLowerCase();
|
|
92
|
+
if (!code) return;
|
|
93
|
+
|
|
94
|
+
const version = def.version;
|
|
95
|
+
const html = def.html;
|
|
96
|
+
const js = def.js;
|
|
97
|
+
const css = def.css;
|
|
98
|
+
|
|
99
|
+
ensureTemplate(code, html);
|
|
100
|
+
|
|
101
|
+
const component = {
|
|
102
|
+
code,
|
|
103
|
+
version,
|
|
104
|
+
css,
|
|
105
|
+
js,
|
|
106
|
+
create: function (props, options) {
|
|
107
|
+
const opts = options || {};
|
|
108
|
+
const mountEl = opts.mountEl || defaultMountTarget();
|
|
109
|
+
const isolation = opts.cssIsolation || state.cssIsolation;
|
|
110
|
+
|
|
111
|
+
const tpl = ensureTemplate(code, html);
|
|
112
|
+
const fragment = tpl.content.cloneNode(true);
|
|
113
|
+
|
|
114
|
+
let templateRootEl;
|
|
115
|
+
let instanceRoot;
|
|
116
|
+
let shadow = null;
|
|
117
|
+
|
|
118
|
+
if (isolation === 'shadow') {
|
|
119
|
+
const c = createShadowRootContainer();
|
|
120
|
+
instanceRoot = c.host;
|
|
121
|
+
shadow = c.shadow;
|
|
122
|
+
templateRootEl = shadow;
|
|
123
|
+
if (css) {
|
|
124
|
+
const style = document.createElement('style');
|
|
125
|
+
style.textContent = toStr(css);
|
|
126
|
+
shadow.appendChild(style);
|
|
127
|
+
}
|
|
128
|
+
shadow.appendChild(fragment);
|
|
129
|
+
} else {
|
|
130
|
+
instanceRoot = document.createElement('div');
|
|
131
|
+
templateRootEl = instanceRoot;
|
|
132
|
+
if (css) injectCssScoped(code, css);
|
|
133
|
+
instanceRoot.appendChild(fragment);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
mountEl.appendChild(instanceRoot);
|
|
137
|
+
|
|
138
|
+
const api = {
|
|
139
|
+
unmount: function () {
|
|
140
|
+
try {
|
|
141
|
+
instanceRoot.remove();
|
|
142
|
+
} catch {}
|
|
143
|
+
},
|
|
144
|
+
mountEl,
|
|
145
|
+
hostEl: instanceRoot,
|
|
146
|
+
shadowRoot: shadow,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const fn = compileComponentJs(js);
|
|
150
|
+
let exported = {};
|
|
151
|
+
try {
|
|
152
|
+
exported = fn(api, templateRootEl, props || {}) || {};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
exported = {
|
|
155
|
+
error: e,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Object.assign({ api }, exported);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
state.components[code] = component;
|
|
164
|
+
uiCmp[code] = component;
|
|
165
|
+
uiComponents[code] = component;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function init(options) {
|
|
169
|
+
const opts = options || {};
|
|
170
|
+
const projectId = toStr(opts.projectId).trim();
|
|
171
|
+
if (!projectId) throw new Error('projectId is required');
|
|
172
|
+
|
|
173
|
+
const apiUrl = normalizeApiUrl(opts.apiUrl);
|
|
174
|
+
const apiKey = opts.apiKey;
|
|
175
|
+
|
|
176
|
+
state.projectId = projectId;
|
|
177
|
+
state.apiKey = apiKey;
|
|
178
|
+
state.apiUrl = apiUrl;
|
|
179
|
+
|
|
180
|
+
const cssIsolation = toStr(opts.cssIsolation || 'scoped').trim().toLowerCase();
|
|
181
|
+
state.cssIsolation = cssIsolation === 'shadow' ? 'shadow' : 'scoped';
|
|
182
|
+
|
|
183
|
+
const base = state.apiUrl;
|
|
184
|
+
const url = base + '/api/ui-components/projects/' + encodeURIComponent(projectId) + '/manifest';
|
|
185
|
+
|
|
186
|
+
const data = await fetchJson(url, buildHeaders(apiKey));
|
|
187
|
+
const items = data && Array.isArray(data.components) ? data.components : [];
|
|
188
|
+
|
|
189
|
+
for (const def of items) {
|
|
190
|
+
registerComponent(def);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
state.initialized = true;
|
|
194
|
+
return { project: data ? data.project : null, count: items.length };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function load(code) {
|
|
198
|
+
const c = toStr(code).trim().toLowerCase();
|
|
199
|
+
if (!c) throw new Error('code is required');
|
|
200
|
+
if (!state.projectId) throw new Error('uiCmp not initialized');
|
|
201
|
+
if (state.components[c]) return state.components[c];
|
|
202
|
+
|
|
203
|
+
const base = state.apiUrl;
|
|
204
|
+
const url =
|
|
205
|
+
base +
|
|
206
|
+
'/api/ui-components/projects/' +
|
|
207
|
+
encodeURIComponent(state.projectId) +
|
|
208
|
+
'/components/' +
|
|
209
|
+
encodeURIComponent(c);
|
|
210
|
+
|
|
211
|
+
const data = await fetchJson(url, buildHeaders(state.apiKey));
|
|
212
|
+
if (!data || !data.component) throw new Error('Component not found');
|
|
213
|
+
|
|
214
|
+
registerComponent(data.component);
|
|
215
|
+
return state.components[c];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const uiCmp = {
|
|
219
|
+
init,
|
|
220
|
+
load,
|
|
221
|
+
_state: state,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const uiComponents = uiCmp;
|
|
225
|
+
|
|
226
|
+
window.uiCmp = uiCmp;
|
|
227
|
+
window.uiComponents = uiComponents;
|
|
228
|
+
})();
|